package org.pine.start.filter;

import cn.hutool.core.text.StrPool;
import com.alibaba.druid.DbType;
import com.alibaba.druid.proxy.jdbc.*;
import com.alibaba.druid.sql.SQLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * 继承自Slf4jLogFilter的Druid SQL日志过滤器类
 * 用于自定义SQL日志的输出格式和行为，基于阿里巴巴的Druid数据库连接池
 * 主要目的是增强日志输出的灵活性和可读性，以及可能的性能优化
 *
 * @author pine
 * @version v1.0
 * @since 2025-04-12 13:23
 */
public class DruildSqlSlf4jLogFilter extends com.alibaba.druid.filter.logging.Slf4jLogFilter {
    private final static Logger logger = LoggerFactory.getLogger(DruildSqlSlf4jLogFilter.class);

    private static final String PREPARED_STATEMENT_PREFIX = "pstmt-";
    private final static String CALLABLE_STATEMENT_PREFIX = "cstmt-";
    private final static String STATEMENT_PREFIX = "stmt-";
    private final static String CONNECTION_PREFIX = "conn-";

    public DruildSqlSlf4jLogFilter() {
        super();
        setStatementSqlFormatOption(new SQLUtils.FormatOption(false, false));
    }

    /**
     * 当Statement执行出错后调用此方法
     *
     * @param statement 执行SQL的Statement代理对象
     * @param sql       执行的SQL语句
     * @param error     执行过程中捕获的异常
     */
    @SuppressWarnings("PMD")
    @Override
    protected void statement_executeErrorAfter(StatementProxy statement, String sql, Throwable error) {
        if (!isStatementLogErrorEnabled()) {
            return;
        }
        String formattedSql = getFormattedSql(statement, sql);
        logger.error("[({}{}, {}) executed error.] SQL:{}", CONNECTION_PREFIX, statement.getConnectionProxy().getId(), stmtId(statement), removeBreakingWhitespace(formattedSql), error);
    }

    @Override
    protected void statementPrepareAfter(PreparedStatementProxy statement) {
        if (isStatementPrepareAfterLogEnabled() && isStatementLogEnabled()) {
            statementLog("{conn-" + statement.getConnectionProxy().getId() + ", pstmt-" + statement.getId() + "} created. " + removeBreakingWhitespace(statement.getSql()));
        }
    }

    @Override
    protected void statementCreateAfter(StatementProxy statement) {
        if (isStatementCreateAfterLogEnabled() && isStatementLogEnabled()) {
            statementLog("{conn-" + statement.getConnectionProxy().getId() + ", stmt-" + statement.getId() + "} created");
        }
    }

    @Override
    protected void statementPrepareCallAfter(CallableStatementProxy statement) {
        if (isStatementPrepareCallAfterLogEnabled() && isStatementLogEnabled()) {
            statementLog("{conn-" + statement.getConnectionProxy().getId() + ", cstmt-" + statement.getId() + "} created. " + statement.getSql());
        }
    }

    @Override
    protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
        logExecutableSql(statement, sql);
    }

    @Override
    protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
        String sql;
        if (statement instanceof PreparedStatementProxy) {
            sql = ((PreparedStatementProxy) statement).getSql();
        } else {
            sql = statement.getBatchSql();
        }

        logExecutableSql(statement, sql);
    }

    @Override
    protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
        logExecutableSql(statement, sql);
    }

    @Override
    protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
        logExecutableSql(statement, sql);
    }

    private String buildMessage(String connectionId, String statementPrefix, String statementId, String sql) {
        return "{" + CONNECTION_PREFIX + connectionId + ", " + statementPrefix + statementId + "} created. SQL:" + sql;
    }

    private void logExecutableSql(StatementProxy statement, String sql) {
        statement.setLastExecuteTimeNano();
        double nanos = statement.getLastExecuteTimeNano();
        double millis = nanos / (1000 * 1000);
        String formattedSql = getFormattedSql(statement, sql);
        logger.info("[({}{}, {}) executed. cost {} millis.] SQL:{}  ", CONNECTION_PREFIX, statement.getConnectionProxy().getId(), stmtId(statement), millis, removeBreakingWhitespace(formattedSql));
    }

    private String stmtId(StatementProxy statement) {
        StringBuilder buf = new StringBuilder();
        if (statement instanceof CallableStatementProxy) {
            buf.append(CALLABLE_STATEMENT_PREFIX);
        } else if (statement instanceof PreparedStatementProxy) {
            buf.append(PREPARED_STATEMENT_PREFIX);
        } else {
            buf.append(STATEMENT_PREFIX);
        }
        buf.append(statement.getId());

        return buf.toString();
    }

    /**
     * 格式化SQL语句
     * 此方法旨在将给定的SQL语句进行格式化，以便在日志输出或者调试时更加清晰易读
     * 它通过移除多余的空格和换行符，同时在关键字和操作符周围保持适当的空格，以达到格式化的目的
     *
     * @param statement 代理声明对象，用于执行SQL语句的对象，此处未使用，但可能在将来或特定情况下需要
     * @param sql       待格式化处理的原始SQL字符串
     * @return 格式化后的SQL字符串
     */
    private String getFormattedSql(StatementProxy statement, String sql) {
        int parametersSize = statement.getParametersSize();
        // 当前sql无参数
        if (parametersSize == 0) {
            return sql;
        }

        List<Object> parameters = new ArrayList<>(parametersSize);
        for (int i = 0; i < parametersSize; ++i) {
            JdbcParameter jdbcParam = statement.getParameter(i);
            parameters.add(jdbcParam != null ? jdbcParam.getValue() : null);
        }
        String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType();
        return SQLUtils.format(sql, DbType.of(dbType), parameters, this.getStatementSqlFormatOption());
    }

    /**
     * 将所有空白符号替换成空格
     *
     * @param original 原始字符串
     * @return 转换之后的字符串
     */
    protected String removeBreakingWhitespace(String original) {
        if (ObjectUtils.isEmpty(original)) {
            return original;
        }
        StringBuilder builder = new StringBuilder(original.length());
        for (char c : original.toCharArray()) {
            if (!Character.isWhitespace(c)) {
                builder.append(c);
            } else {
                builder.append(StrPool.C_SPACE);
            }
        }
        return builder.toString();
    }

}
